package com.directions.android.screens;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.directions.android.BundleUtils;
import com.directions.android.DialogUtils;
import com.directions.android.DialogUtils.TextInputListener;
import com.directions.android.R;
import com.directions.android.storage.sqlite.LocationSQLiteHelper;
import com.directions.route.Routing;
import com.directions.route.Routing.TravelMode;
import com.directions.route.RoutingListener;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener;
import com.google.android.gms.maps.GoogleMap.OnMapClickListener;
import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

public class MapFragment extends Fragment implements RoutingListener,
		ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
	public static final String LOG_TAG = "PlaceholderFragmentMap";

	// Used to know which marker is associated with which Location, there isn't
	// a better way to do this since the markers don't have a "setUserObject"
	// option.
	private Map<String, com.directions.android.storage.Location> markersMap = new HashMap<String, com.directions.android.storage.Location>();

	// These settings are the same as the settings for the map. They will in
	// fact give you updates at
	// the maximal rates currently possible.
	private static final LocationRequest REQUEST = LocationRequest.create()
			.setInterval(5000) // 5 seconds
			.setFastestInterval(16) // 16ms = 60fps
			.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

	// Global constants
	public static final String APP_TAG = "Location";
	/*
	 * Define a request code to send to Google Play services This code is
	 * returned in Activity.onActivityResult
	 */
	private final static int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;

	private ProgressDialog mPleaseWaitDialog = null;
	private boolean isOrigCurrPos, isDstCurrPos;
	private boolean centerInCurrentLocation;
	private LocationClient mLocationClient;
	private LocationSQLiteHelper sqlite;
	private TravelMode mode;
	private GoogleMap map;
	private LatLng orig;
	private LatLng dest;

	public static Fragment getInstance(Bundle data) {
		Fragment mapFragment = new MapFragment();
		if (data != null)
			mapFragment.setArguments(data);
		return mapFragment;
	}

	/**
	 * This activity loads a map and then displays the route and markers on it.
	 */
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		super.onCreateView(inflater, container, savedInstanceState);
		View rootView = inflater.inflate(R.layout.map, container, false);
		return rootView;
	}

	@Override
	public void onResume() {
		super.onResume();
		if (servicesConnected()) {
			setUpSQLiteIfNeeded();
			setUpMapIfNeeded();
		}
	}

	// Define a DialogFragment that displays the error dialog
	public static class ErrorDialogFragment extends DialogFragment {
		// Global field to contain the error dialog
		private Dialog mDialog;

		// Default constructor. Sets the dialog field to null
		public ErrorDialogFragment() {
			super();
			mDialog = null;
		}

		// Set the dialog to display
		public void setDialog(Dialog dialog) {
			mDialog = dialog;
		}

		// Return a Dialog to the DialogFragment.
		@Override
		public Dialog onCreateDialog(Bundle savedInstanceState) {
			return mDialog;
		}
	}

	/**
	 * Verify that Google Play services is available before making a request.
	 * 
	 * @return true if Google Play services is available, otherwise false
	 */
	private boolean servicesConnected() {
		// Check that Google Play services is available
		int resultCode = GooglePlayServicesUtil
				.isGooglePlayServicesAvailable(getActivity());
		// If Google Play services is available
		if (ConnectionResult.SUCCESS == resultCode) {
			// In debug mode, log the status
			Log.d(APP_TAG, "Google Play services is available.");
			// Continue
			return true;
			// Google Play services was not available for some reason
		} else {
			// Display an error dialog
			Dialog dialog = GooglePlayServicesUtil.getErrorDialog(resultCode,
					getActivity(), CONNECTION_FAILURE_RESOLUTION_REQUEST);
			if (dialog != null) {
				ErrorDialogFragment errorFragment = new ErrorDialogFragment();
				errorFragment.setDialog(dialog);
				errorFragment.show(getActivity().getSupportFragmentManager(),
						APP_TAG);
			}
			return false;
		}
	}

	@Override
	public void onPause() {
		super.onPause();
		if (mLocationClient != null && mLocationClient.isConnected()) {
			mLocationClient.disconnect();
		}
	}

	private void setUpSQLiteIfNeeded() {
		if (sqlite == null) {
			sqlite = new LocationSQLiteHelper(getActivity());
		}
	}

	private void setUpMapIfNeeded() {
		// Do a null check to confirm that we have not already instantiated the
		// map.
		if (map == null) {

			// Try to obtain the map from the SupportMapFragment.
			SupportMapFragment fm = (SupportMapFragment) getActivity()
					.getSupportFragmentManager().findFragmentById(R.id.map);
			map = fm.getMap();
			// Check if we were successful in obtaining the map.
			if (map != null) {
				map.clear();

				map.setMyLocationEnabled(true);
				map.setOnMapClickListener(new OnMapClickListener() {
					private boolean starting = true;

					@Override
					public void onMapClick(LatLng point) {
						if (starting) {
							orig = point;
						} else {
							dest = point;
						}
						starting = !starting;
						if (orig != null && dest != null)
							new Routing(TravelMode.DRIVING).setTravelMode(mode)
									.setListener(MapFragment.this)
									.execute(orig, dest);
					}
				});

				map.setOnMapLongClickListener(new OnMapLongClickListener() {

					@Override
					public void onMapLongClick(final LatLng point) {
						DialogUtils.getTextInput(getActivity(),
								getString(R.string.add_a_location),
								getString(R.string.location_title), null,
								new TextInputListener() {

									@Override
									public void input(String msg) {
										if (msg == null || msg.isEmpty()
												|| msg.trim().isEmpty())
											return;
										com.directions.android.storage.Location createdMarker = new com.directions.android.storage.Location(
												-1, point.latitude,
												point.longitude, msg);
										sqlite.insert(createdMarker);
										addMarkerToMap(
												new LatLng(point.latitude,
														point.longitude), msg,
												R.drawable.start_orange, true,
												createdMarker);
									}

									@Override
									public void canceled() {

									}
								});
					}
				});

				// Zoom in, animating the camera.
				map.animateCamera(CameraUpdateFactory.zoomTo(10));

				final Bundle mapInfo = getArguments();

				// If we have no arguments, e.g. starting the application where
				// the initial screen is this MapFragment.
				if (mapInfo == null) {
					// We center our camera in our position
					centerInCurrentLocation = true;
					displayMarkersIfNeeded();
					askForCurrentLocation();
					return;
				}

				isOrigCurrPos = mapInfo.getBoolean(BundleUtils.CURR_POS_ORIG);
				isDstCurrPos = mapInfo.getBoolean(BundleUtils.CURR_POS_DST);

				double origLon = mapInfo.getDouble(BundleUtils.ORIG_LONG);
				double origLat = mapInfo.getDouble(BundleUtils.ORIG_LAT);

				// If for some strange reason our bundle doesn't have origin
				// latitude and longitude we don't process any route
				if (isOrigCurrPos || isDstCurrPos || origLat == 0.0D
						&& origLon == 0.0D) {
					displayMarkersIfNeeded();
					askForCurrentLocation();
					return;
				}

				orig = new LatLng(origLat, origLon);

				double dstLon = mapInfo.getDouble(BundleUtils.DST_LONG);
				double dstLat = mapInfo.getDouble(BundleUtils.DST_LAT);
				if (dstLat != 0.0D && dstLon != 0.0D) {
					dest = new LatLng(dstLat, dstLon);

					mode = BundleUtils.strToTravelMode(getActivity(),
							mapInfo.getString(BundleUtils.ROUTE));
					new Routing(TravelMode.DRIVING).setTravelMode(mode)
							.setListener(this).execute(orig, dest);

					Log.d(LOG_TAG, "Moving from " + orig.toString() + ", to "
							+ dest.toString());
				} else {
					// If we only have the origin defined means we're coming
					// from MarkersFragment clicking on a location
					displayMarkersIfNeeded(new com.directions.android.storage.Location(
							mapInfo.getInt(BundleUtils.MARKER_ID),
							orig.latitude, orig.longitude,
							mapInfo.getString(BundleUtils.MARKER_TITLE)));
				}

				// Move the camera instantly to location.
				map.moveCamera(CameraUpdateFactory.newLatLng(orig));
				// Zoom in, animating the camera.
				map.animateCamera(CameraUpdateFactory.zoomTo(15), 2000, null);
			}
		}
	}

	private void askForCurrentLocation() {
		setUpLocationClientIfNeeded();
		showLocationPendingDialog();
		mLocationClient.connect();
	}

	private void displayMarkersIfNeeded() {
		displayMarkersIfNeeded(null);
	}

	private void displayMarkersIfNeeded(
			com.directions.android.storage.Location mark) {
		SharedPreferences sp = PreferenceManager
				.getDefaultSharedPreferences(getActivity());
		boolean showMarkers = sp.getBoolean(PreferenceFragment.MARKERS, true);
		markersMap.clear();
		if (showMarkers) {

			List<com.directions.android.storage.Location> myMarkers = null;
			try {
				myMarkers = sqlite.getList(getActivity());
			} catch (Exception e) {
				return;
			}

			for (int i = 0, size = myMarkers.size(); i < size; ++i) {
				com.directions.android.storage.Location marker = myMarkers
						.get(i);
				if (mark == null || !mark.equals(marker)) {
					addMarkerToMap(
							new LatLng(marker.getLatitude(),
									marker.getLongitude()), marker.getTitle(),
							R.drawable.start_orange, true, marker);
				}
			}
		}
		if (mark != null) {
			addMarkerToMap(new LatLng(mark.getLatitude(), mark.getLongitude()),
					mark.getTitle(), R.drawable.start_red, true, mark)
					.showInfoWindow();
		}

		// Dragging markers
		map.setOnMarkerDragListener(new OnMarkerDragListener() {

			@Override
			public void onMarkerDrag(Marker marker) {

			}

			@Override
			public void onMarkerDragEnd(Marker marker) {
				com.directions.android.storage.Location loc = markersMap
						.get(marker.getId());
				if (loc != null) {
					// Update in our DB the new position.
					LatLng latlng = marker.getPosition();
					loc.setLatitude(latlng.latitude);
					loc.setLongitude(latlng.longitude);
					sqlite.updateRow(loc);
				}
			}

			@Override
			public void onMarkerDragStart(Marker marker) {

			}
		});

		// Clicking markers info window
		map.setOnInfoWindowClickListener(new OnInfoWindowClickListener() {

			@Override
			public void onInfoWindowClick(final Marker marker) {
				final com.directions.android.storage.Location loc = markersMap
						.get(marker.getId());
				if (loc != null) {
					DialogUtils.getTextInput(getActivity(),
							getString(R.string.add_a_location),
							getString(R.string.location_title), loc.getTitle(),
							new TextInputListener() {

								@Override
								public void input(String msg) {
									if (msg == null || msg.isEmpty()
											|| msg.trim().isEmpty())
										return;
									loc.setTitle(msg);
									// Update in our DB the title.
									sqlite.updateRow(loc);
									marker.setTitle(msg);
									marker.showInfoWindow();
								}

								@Override
								public void canceled() {

								}
							});
				}
			}
		});
	}

	private Marker addMarkerToMap(LatLng pos, int drawableId) {
		return addMarkerToMap(pos, null, drawableId, false, null);
	}

	private Marker addMarkerToMap(LatLng pos, String title, int drawableId,
			boolean draggable, com.directions.android.storage.Location loc) {
		if (map == null)
			return null;

		MarkerOptions options = new MarkerOptions();
		if (title != null)
			options.title(title);
		options.position(pos);
		options.draggable(draggable);
		options.icon(BitmapDescriptorFactory.fromResource(drawableId));
		Marker marker = map.addMarker(options);

		if (loc != null)
			markersMap.put(marker.getId(), loc);

		return marker;
	}

	private void setUpLocationClientIfNeeded() {
		if (mLocationClient == null) {
			mLocationClient = new LocationClient(getActivity()
					.getApplicationContext(), this, // ConnectionCallbacks
					this); // OnConnectionFailedListener
		}
	}

	@Override
	public void onRoutingFailure() {
		cancelLocationPendingDialog();
		// The Routing request failed
		DialogUtils.showConfirmationAlert(getActivity(),
				getString(R.string.no_route_found), getString(R.string.error));
	}

	public void showLocationPendingDialog() {
		if (mPleaseWaitDialog != null) {
			return;
		}

		mPleaseWaitDialog = new ProgressDialog(getActivity());
		mPleaseWaitDialog
				.setMessage(getString(R.string.searching_for_location));
		mPleaseWaitDialog.setTitle(R.string.please_wait);
		mPleaseWaitDialog.setIndeterminate(true);
		mPleaseWaitDialog.show();
	}

	public void cancelLocationPendingDialog() {
		if (mPleaseWaitDialog != null) {
			mPleaseWaitDialog.dismiss();
			mPleaseWaitDialog = null;
		}
	}

	@Override
	public void onRoutingStart() {
		// The Routing Request starts
		showLocationPendingDialog();
	}

	@Override
	public void onRoutingSuccess(PolylineOptions mPolyOptions) {
		cancelLocationPendingDialog();
		if (map == null)
			return;

		map.clear();
		displayMarkersIfNeeded();
		PolylineOptions polyoptions = new PolylineOptions();
		polyoptions.color(Color.BLUE);
		polyoptions.width(10);
		polyoptions.addAll(mPolyOptions.getPoints());
		map.addPolyline(polyoptions);

		// Start marker
		addMarkerToMap(orig, R.drawable.start_blue);

		// End marker
		addMarkerToMap(dest, R.drawable.end_green);
	}

	public void onDestroyView() {
		super.onDestroyView();
		Fragment fragment = (getFragmentManager().findFragmentById(R.id.map));
		FragmentTransaction ft = getActivity().getSupportFragmentManager()
				.beginTransaction();
		ft.remove(fragment);
		ft.commit();
		map = null;
	}

	@Override
	public void onLocationChanged(Location location) {
		if (centerInCurrentLocation) {
			centerInCurrentLocation = false;
			if (map != null) {
				cancelLocationPendingDialog();
				map.moveCamera(CameraUpdateFactory.newLatLng(new LatLng(
						location.getLatitude(), location.getLongitude())));
				// Zoom in, animating the camera.
				map.animateCamera(CameraUpdateFactory.zoomTo(15), 2000, null);
			}
		} else {
			if (map == null)
				return;

			Bundle mapInfo = getArguments();

			if (isOrigCurrPos)
				orig = new LatLng(location.getLatitude(),
						location.getLongitude());
			else {

				double origLon = mapInfo.getDouble(BundleUtils.ORIG_LONG);
				double origLat = mapInfo.getDouble(BundleUtils.ORIG_LAT);

				if (origLat != 0.0D || origLon != 0.0D) {
					orig = new LatLng(origLat, origLon);
				}
			}

			if (isDstCurrPos)
				dest = new LatLng(location.getLatitude(),
						location.getLongitude());
			else {

				double dstLon = mapInfo.getDouble(BundleUtils.DST_LONG);
				double dstLat = mapInfo.getDouble(BundleUtils.DST_LAT);

				if (dstLon != 0.0D || dstLat != 0.0D) {
					dest = new LatLng(dstLat, dstLon);
				}
			}

			if (orig != null && dest != null) {
				map.moveCamera(CameraUpdateFactory.newLatLng(orig));
				// Zoom in, animating the camera.
				map.animateCamera(CameraUpdateFactory.zoomTo(15), 2000, null);
				mode = BundleUtils.strToTravelMode(getActivity(),
						mapInfo.getString(BundleUtils.ROUTE));
				new Routing(TravelMode.DRIVING).setTravelMode(mode)
						.setListener(this).execute(orig, dest);
			} else {
				cancelLocationPendingDialog();
			}
		}

		if (mLocationClient != null)
			mLocationClient.disconnect();
	}

	/**
	 * Callback called when connected to GCore. Implementation of
	 * {@link ConnectionCallbacks}.
	 */
	@Override
	public void onConnected(Bundle connectionHint) {
		mLocationClient.requestLocationUpdates(REQUEST, this); // LocationListener
	}

	/**
	 * Callback called when disconnected from GCore. Implementation of
	 * {@link ConnectionCallbacks}.
	 */
	@Override
	public void onDisconnected() {
		// Do nothing
	}

	/**
	 * Implementation of {@link OnConnectionFailedListener}.
	 */
	@Override
	public void onConnectionFailed(ConnectionResult result) {
		/*
		 * Google Play services can resolve some errors it detects. If the error
		 * has a resolution, try sending an Intent to start a Google Play
		 * services activity that can resolve error.
		 */
		if (result.hasResolution()) {
			try {
				// Start an Activity that tries to resolve the error
				result.startResolutionForResult(getActivity(),
						CONNECTION_FAILURE_RESOLUTION_REQUEST);
				/*
				 * Thrown if Google Play services canceled the original
				 * PendingIntent
				 */
			} catch (IntentSender.SendIntentException e) {
				// Log the error
				e.printStackTrace();
			}
		} else {
			/*
			 * If no resolution is available, display a dialog to the user with
			 * the error.
			 */
			DialogUtils.showConfirmationAlert(getActivity(),
					getString(R.string.couldnt_connect_to_retrieve_location),
					getString(R.string.location_failed));
		}
	}
}
